--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 4b9c16f6c3c8de59bf94da550189b2484b396da8
Parents : 546a5e8
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-23T17:46:08-05:00
feat(microphone): update audio recording error handling and update worklet import method
Changes
3 files changed, 51 insertions(+), 6 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index dc362a70..a1fecb74 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -5167,12 +5167,33 @@ export default {
},
buildAudioRecordingFailureMessage() {
if (!navigator?.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function") {
- return `${this.$t("messages.failed_start_recording")} (microphone API unavailable in this webview context)`;
+ return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_mediadevices")}`;
}
- if (typeof MediaRecorder !== "function") {
- return `${this.$t("messages.failed_start_recording")} (MediaRecorder not supported on this device)`;
+ const AudioContextCtor = globalThis.AudioContext || globalThis.webkitAudioContext;
+ if (typeof AudioContextCtor !== "function") {
+ return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_web_audio")}`;
}
- return this.$t("messages.failed_start_recording");
+ let probe = null;
+ try {
+ probe = new AudioContextCtor();
+ if (!probe.audioWorklet || typeof probe.audioWorklet.addModule !== "function") {
+ return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_audio_worklet")}`;
+ }
+ } catch {
+ return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_web_audio")}`;
+ } finally {
+ try {
+ if (probe && typeof probe.close === "function") {
+ const closed = probe.close();
+ if (closed && typeof closed.catch === "function") {
+ void closed.catch(() => {});
+ }
+ }
+ } catch {
+ // ignore
+ }
+ }
+ return `${this.$t("messages.failed_start_recording")}. ${this.$t("messages.failed_start_recording_help_permission")}`;
},
removeFileAttachment: function (file) {
this.newMessageFiles = this.newMessageFiles.filter((newMessageFile) => {
diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.js b/meshchatx/src/frontend/js/MicrophoneRecorder.js
index b96272a8..d568ec3d 100644
--- a/meshchatx/src/frontend/js/MicrophoneRecorder.js
+++ b/meshchatx/src/frontend/js/MicrophoneRecorder.js
@@ -6,7 +6,7 @@
* encode the audio to OGG/Opus using LXST's OpusFileSink without relying
* on ffmpeg or any browser MediaRecorder container output.
*/
-import microphoneRecorderWorkletUrl from "./MicrophoneRecorder.worklet.js?url";
+import microphoneRecorderWorkletSource from "./MicrophoneRecorder.worklet.js?raw";
class MicrophoneRecorder {
constructor() {
@@ -18,6 +18,7 @@ class MicrophoneRecorder {
this.pcmChunks = [];
this.sampleRate = 0;
this.channels = 1;
+ this._workletBlobUrl = null;
}
cleanupMediaStream() {
@@ -57,6 +58,14 @@ class MicrophoneRecorder {
} catch {
// ignore disconnect failures
}
+ if (this._workletBlobUrl) {
+ try {
+ URL.revokeObjectURL(this._workletBlobUrl);
+ } catch {
+ // ignore
+ }
+ this._workletBlobUrl = null;
+ }
if (this.audioContext && typeof this.audioContext.close === "function") {
try {
const closeResult = this.audioContext.close();
@@ -110,7 +119,21 @@ class MicrophoneRecorder {
return false;
}
- await this.audioContext.audioWorklet.addModule(microphoneRecorderWorkletUrl);
+ const workletBlob = new Blob([microphoneRecorderWorkletSource], {
+ type: "application/javascript",
+ });
+ this._workletBlobUrl = URL.createObjectURL(workletBlob);
+ try {
+ await this.audioContext.audioWorklet.addModule(this._workletBlobUrl);
+ } catch {
+ try {
+ URL.revokeObjectURL(this._workletBlobUrl);
+ } catch {
+ // ignore
+ }
+ this._workletBlobUrl = null;
+ throw new Error("AudioWorklet addModule failed");
+ }
this.processorNode = new AudioWorkletNode(this.audioContext, "microphone-pcm-float", {
numberOfInputs: 1,
numberOfOutputs: 1,
diff --git a/tests/frontend/MicrophoneRecorder.test.js b/tests/frontend/MicrophoneRecorder.test.js
index 0f5421b4..470a279a 100644
--- a/tests/frontend/MicrophoneRecorder.test.js
+++ b/tests/frontend/MicrophoneRecorder.test.js
@@ -133,6 +133,7 @@ describe("MicrophoneRecorder", () => {
expect(audio.ctx.createMediaStreamSource).toHaveBeenCalledTimes(1);
expect(audio.ctx.audioWorklet.addModule).toHaveBeenCalledTimes(1);
+ expect(audio.ctx.audioWorklet.addModule.mock.calls[0][0]).toMatch(/^blob:/);
expect(globalThis.AudioWorkletNode).toHaveBeenCalledWith(
audio.ctx,
"microphone-pcm-float",
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────